// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * A class that tries to locate name servers and the search path to be appended
 * to unqualified names.
 * 
 * The following are attempted, in order, until one succeeds.
 * <UL>
 * <LI>The properties 'dns.server' and 'dns.search' (comma delimited lists) are
 * checked. The servers can either be IP addresses or hostnames (which are
 * resolved using Java's built in DNS support).
 * <LI>The sun.net.dns.ResolverConfiguration class is queried.
 * <LI>On Unix, /etc/resolv.conf is parsed.
 * <LI>On Windows, ipconfig/winipcfg is called and its output parsed. This may
 * fail for non-English versions on Windows.
 * <LI>"localhost" is used as the nameserver, and the search path is empty.
 * </UL>
 * 
 * These routines will be called internally when creating Resolvers/Lookups
 * without explicitly specifying server names, and can also be called directly
 * if desired.
 * 
 * @author Brian Wellington
 * @author <a href="mailto:yannick@meudal.net">Yannick Meudal</a>
 * @author <a href="mailto:arnt@gulbrandsen.priv.no">Arnt Gulbrandsen</a>
 */

public class ResolverConfig {

	private String[] servers = null;
	private Name[] searchlist = null;
	private int ndots = -1;

	private static ResolverConfig currentConfig;

	static {
		refresh();
	}

	public ResolverConfig() {
		if (findProperty())
			return;
		if (findSunJVM())
			return;
		if (servers == null || searchlist == null) {
			String OS = System.getProperty("os.name");
			String vendor = System.getProperty("java.vendor");
			if (OS.indexOf("Windows") != -1) {
				if (OS.indexOf("95") != -1 || OS.indexOf("98") != -1
						|| OS.indexOf("ME") != -1)
					find95();
				else
					findNT();
			} else if (OS.indexOf("NetWare") != -1) {
				findNetware();
			} else if (vendor.indexOf("Android") != -1) {
				findAndroid();
			} else {
				findUnix();
			}
		}
	}

	private void addServer(String server, List list) {
		if (list.contains(server))
			return;
		if (Options.check("verbose"))
			System.out.println("adding server " + server);
		list.add(server);
	}

	private void addSearch(String search, List list) {
		Name name;
		if (Options.check("verbose"))
			System.out.println("adding search " + search);
		try {
			name = Name.fromString(search, Name.root);
		} catch (TextParseException e) {
			return;
		}
		if (list.contains(name))
			return;
		list.add(name);
	}

	private int parseNdots(String token) {
		token = token.substring(6);
		try {
			int ndots = Integer.parseInt(token);
			if (ndots >= 0) {
				if (Options.check("verbose"))
					System.out.println("setting ndots " + token);
				return ndots;
			}
		} catch (NumberFormatException e) {
		}
		return -1;
	}

	private void configureFromLists(List lserver, List lsearch) {
		if (servers == null && lserver.size() > 0)
			servers = (String[]) lserver.toArray(new String[0]);
		if (searchlist == null && lsearch.size() > 0)
			searchlist = (Name[]) lsearch.toArray(new Name[0]);
	}

	private void configureNdots(int lndots) {
		if (ndots < 0 && lndots > 0)
			ndots = lndots;
	}

	/**
	 * Looks in the system properties to find servers and a search path. Servers
	 * are defined by dns.server=server1,server2... The search path is defined
	 * by dns.search=domain1,domain2...
	 */
	private boolean findProperty() {
		String prop;
		List lserver = new ArrayList(0);
		List lsearch = new ArrayList(0);
		StringTokenizer st;

		prop = System.getProperty("dns.server");
		if (prop != null) {
			st = new StringTokenizer(prop, ",");
			while (st.hasMoreTokens())
				addServer(st.nextToken(), lserver);
		}

		prop = System.getProperty("dns.search");
		if (prop != null) {
			st = new StringTokenizer(prop, ",");
			while (st.hasMoreTokens())
				addSearch(st.nextToken(), lsearch);
		}
		configureFromLists(lserver, lsearch);
		return (servers != null && searchlist != null);
	}

	/**
	 * Uses the undocumented Sun DNS implementation to determine the
	 * configuration. This doesn't work or even compile with all JVMs (gcj, for
	 * example).
	 */
	private boolean findSunJVM() {
		List lserver = new ArrayList(0);
		List lserver_tmp;
		List lsearch = new ArrayList(0);
		List lsearch_tmp;

		try {
			Class[] noClasses = new Class[0];
			Object[] noObjects = new Object[0];
			String resConfName = "sun.net.dns.ResolverConfiguration";
			Class resConfClass = Class.forName(resConfName);
			Object resConf;

			// ResolverConfiguration resConf = ResolverConfiguration.open();
			Method open = resConfClass.getDeclaredMethod("open", noClasses);
			resConf = open.invoke(null, noObjects);

			// lserver_tmp = resConf.nameservers();
			Method nameservers = resConfClass.getMethod("nameservers",
					noClasses);
			lserver_tmp = (List) nameservers.invoke(resConf, noObjects);

			// lsearch_tmp = resConf.searchlist();
			Method searchlist = resConfClass.getMethod("searchlist", noClasses);
			lsearch_tmp = (List) searchlist.invoke(resConf, noObjects);
		} catch (Exception e) {
			return false;
		}

		if (lserver_tmp.size() == 0)
			return false;

		if (lserver_tmp.size() > 0) {
			Iterator it = lserver_tmp.iterator();
			while (it.hasNext())
				addServer((String) it.next(), lserver);
		}

		if (lsearch_tmp.size() > 0) {
			Iterator it = lsearch_tmp.iterator();
			while (it.hasNext())
				addSearch((String) it.next(), lsearch);
		}
		configureFromLists(lserver, lsearch);
		return true;
	}

	/**
	 * Looks in /etc/resolv.conf to find servers and a search path. "nameserver"
	 * lines specify servers. "domain" and "search" lines define the search
	 * path.
	 */
	private void findResolvConf(String file) {
		InputStream in = null;
		try {
			in = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			return;
		}
		InputStreamReader isr = new InputStreamReader(in);
		BufferedReader br = new BufferedReader(isr);
		List lserver = new ArrayList(0);
		List lsearch = new ArrayList(0);
		int lndots = -1;
		try {
			String line;
			while ((line = br.readLine()) != null) {
				if (line.startsWith("nameserver")) {
					StringTokenizer st = new StringTokenizer(line);
					st.nextToken(); /* skip nameserver */
					addServer(st.nextToken(), lserver);
				} else if (line.startsWith("domain")) {
					StringTokenizer st = new StringTokenizer(line);
					st.nextToken(); /* skip domain */
					if (!st.hasMoreTokens())
						continue;
					if (lsearch.isEmpty())
						addSearch(st.nextToken(), lsearch);
				} else if (line.startsWith("search")) {
					if (!lsearch.isEmpty())
						lsearch.clear();
					StringTokenizer st = new StringTokenizer(line);
					st.nextToken(); /* skip search */
					while (st.hasMoreTokens())
						addSearch(st.nextToken(), lsearch);
				} else if (line.startsWith("options")) {
					StringTokenizer st = new StringTokenizer(line);
					st.nextToken(); /* skip options */
					while (st.hasMoreTokens()) {
						String token = st.nextToken();
						if (token.startsWith("ndots:")) {
							lndots = parseNdots(token);
						}
					}
				}
			}
			br.close();
		} catch (IOException e) {
		}

		configureFromLists(lserver, lsearch);
		configureNdots(lndots);
	}

	private void findUnix() {
		findResolvConf("/etc/resolv.conf");
	}

	private void findNetware() {
		findResolvConf("sys:/etc/resolv.cfg");
	}

	/**
	 * Parses the output of winipcfg or ipconfig.
	 */
	private void findWin(InputStream in, Locale locale) {
		String packageName = ResolverConfig.class.getPackage().getName();
		String resPackageName = packageName + ".windows.DNSServer";
		ResourceBundle res;
		if (locale != null)
			res = ResourceBundle.getBundle(resPackageName, locale);
		else
			res = ResourceBundle.getBundle(resPackageName);

		String host_name = res.getString("host_name");
		String primary_dns_suffix = res.getString("primary_dns_suffix");
		String dns_suffix = res.getString("dns_suffix");
		String dns_servers = res.getString("dns_servers");

		BufferedReader br = new BufferedReader(new InputStreamReader(in));
		try {
			List lserver = new ArrayList();
			List lsearch = new ArrayList();
			String line = null;
			boolean readingServers = false;
			boolean readingSearches = false;
			while ((line = br.readLine()) != null) {
				StringTokenizer st = new StringTokenizer(line);
				if (!st.hasMoreTokens()) {
					readingServers = false;
					readingSearches = false;
					continue;
				}
				String s = st.nextToken();
				if (line.indexOf(":") != -1) {
					readingServers = false;
					readingSearches = false;
				}

				if (line.indexOf(host_name) != -1) {
					while (st.hasMoreTokens())
						s = st.nextToken();
					Name name;
					try {
						name = Name.fromString(s, null);
					} catch (TextParseException e) {
						continue;
					}
					if (name.labels() == 1)
						continue;
					addSearch(s, lsearch);
				} else if (line.indexOf(primary_dns_suffix) != -1) {
					while (st.hasMoreTokens())
						s = st.nextToken();
					if (s.equals(":"))
						continue;
					addSearch(s, lsearch);
					readingSearches = true;
				} else if (readingSearches || line.indexOf(dns_suffix) != -1) {
					while (st.hasMoreTokens())
						s = st.nextToken();
					if (s.equals(":"))
						continue;
					addSearch(s, lsearch);
					readingSearches = true;
				} else if (readingServers || line.indexOf(dns_servers) != -1) {
					while (st.hasMoreTokens())
						s = st.nextToken();
					if (s.equals(":"))
						continue;
					addServer(s, lserver);
					readingServers = true;
				}
			}

			configureFromLists(lserver, lsearch);
		} catch (IOException e) {
		}
		return;
	}

	private void findWin(InputStream in) {
		String property = "org.xbill.DNS.windows.parse.buffer";
		final int defaultBufSize = 8 * 1024;
		int bufSize = Integer.getInteger(property, defaultBufSize).intValue();
		BufferedInputStream b = new BufferedInputStream(in, bufSize);
		b.mark(bufSize);
		findWin(b, null);
		if (servers == null) {
			try {
				b.reset();
			} catch (IOException e) {
				return;
			}
			findWin(b, new Locale("", ""));
		}
	}

	/**
	 * Calls winipcfg and parses the result to find servers and a search path.
	 */
	private void find95() {
		String s = "winipcfg.out";
		try {
			Process p;
			p = Runtime.getRuntime().exec("winipcfg /all /batch " + s);
			p.waitFor();
			File f = new File(s);
			findWin(new FileInputStream(f));
			new File(s).delete();
		} catch (Exception e) {
			return;
		}
	}

	/**
	 * Calls ipconfig and parses the result to find servers and a search path.
	 */
	private void findNT() {
		try {
			Process p;
			p = Runtime.getRuntime().exec("ipconfig /all");
			findWin(p.getInputStream());
			p.destroy();
		} catch (Exception e) {
			return;
		}
	}

	/**
	 * Parses the output of getprop, which is the only way to get DNS info on
	 * Android. getprop might disappear in future releases, so this code comes
	 * with a use-by date.
	 */
	private void findAndroid() {
		// This originally looked for all lines containing .dns; but
		// http://code.google.com/p/android/issues/detail?id=2207#c73
		// indicates that net.dns* should always be the active nameservers, so
		// we use those.
		String re1 = "^\\d+(\\.\\d+){3}$";
		String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$";
		try {
			ArrayList lserver = new ArrayList();
			ArrayList lsearch = new ArrayList();
			String line;
			Process p = Runtime.getRuntime().exec("getprop");
			InputStream in = p.getInputStream();
			InputStreamReader isr = new InputStreamReader(in);
			BufferedReader br = new BufferedReader(isr);
			while ((line = br.readLine()) != null) {
				StringTokenizer t = new StringTokenizer(line, ":");
				String name = t.nextToken();
				if (name.indexOf("net.dns") > -1) {
					String v = t.nextToken();
					v = v.replaceAll("[ \\[\\]]", "");
					if ((v.matches(re1) || v.matches(re2))
							&& !lserver.contains(v))
						lserver.add(v);
				}
			}
			configureFromLists(lserver, lsearch);
		} catch (Exception e) {
			// ignore resolutely
		}
	}

	/** Returns all located servers */
	public String[] servers() {
		return servers;
	}

	/** Returns the first located server */
	public String server() {
		if (servers == null)
			return null;
		return servers[0];
	}

	/** Returns all entries in the located search path */
	public Name[] searchPath() {
		return searchlist;
	}

	/**
	 * Returns the located ndots value, or the default (1) if not configured.
	 * Note that ndots can only be configured in a resolv.conf file, and will
	 * only take effect if ResolverConfig uses resolv.conf directly (that is, if
	 * the JVM does not include the sun.net.dns.ResolverConfiguration class).
	 */
	public int ndots() {
		if (ndots < 0)
			return 1;
		return ndots;
	}

	/** Gets the current configuration */
	public static synchronized ResolverConfig getCurrentConfig() {
		return currentConfig;
	}

	/** Gets the current configuration */
	public static void refresh() {
		ResolverConfig newConfig = new ResolverConfig();
		synchronized (ResolverConfig.class) {
			currentConfig = newConfig;
		}
	}

}
